package com.hup.utils.io.audio;

import lombok.Getter;
import lombok.Setter;
import lombok.extern.log4j.Log4j2;

import javax.sound.sampled.*;
import javax.sound.sampled.LineEvent.Type;

/**
 * 音频播放任务类
 * <pre>
 * 资料
 *  https://www.iteye.com/blog/i2534-775167
 *  https://blog.csdn.net/weixin_34014277/article/details/91747994
 *  https://docs.oracle.com/javase/tutorial/sound/index.html
 * </pre>
 *
 * @author hugan
 * @date 2020/5/30
 */
@Log4j2
public class ClipPlayTask implements AudioPlayTask, LineListener {

    private final TaskListener taskListener;
    @Getter
    private final AudioInfo audioInfo;
    private Clip clip;
    /**
     * 主增益;不是音量控制,但可以将就实现音量高低
     * nullable?!
     * <pre>
     *  {@link FloatControl#setValue(float)}:
     *  {@link FloatControl#toString()}:查看范围
     *  https://zx.ingping.com/c_3/4140.html
     * </pre>
     */
    @Getter
    private FloatControl masterGain;

    private ApplyingState state;

    /**
     * 循环播放
     */
    @Setter
    @Getter
    private boolean loop;

    /**
     * true-暂停中
     */
    @Getter
    private boolean pausing;

    public ClipPlayTask(String resourcePath, TaskListener<ClipPlayTask> taskListener) {
        this.taskListener = taskListener;
        AudioInfo temp = null;
        try {
            temp = new AudioInfo(resourcePath, audioInputStream -> {
                DataLine.Info info = new DataLine.Info(Clip.class, audioInputStream.getFormat());
                clip = (Clip) AudioSystem.getLine(info);
                clip.open(audioInputStream);
            });
            state = new ApplyingState();
            clip.addLineListener(this);
            log.debug("frame={}", clip.getFrameLength());
            masterGain = (FloatControl) clip.getControl(FloatControl.Type.MASTER_GAIN);
            log.debug("volumeControl={}", masterGain);
            if (taskListener != null) taskListener.onGetInfo(temp, null, this);
        } catch (Exception e) {
            if (taskListener != null) taskListener.onGetInfo(null, e, this);
        }
        audioInfo = temp;
    }

    /**
     * 开始播放;
     * <pre>
     * [未开始,播放中,暂停中]调用本方法,都是从头开始播放;
     * 触发通知{@link TaskListener#onStartPlay()}
     * </pre>
     * @apiNote 如果是频繁/快速播放的短音效,如快速调用5次,则可能只听到3次;╮(╯_╰)╭SourceDataLine则不会有此问题
     */
    @Override
    public void play() {
        if (audioInfo == null) return;
        if (!clip.isRunning()) {
            state.reset();
            state.starting = true;
            pausing = false;
            clip.setFramePosition(0);//在stop时间中重置时,会偶尔不生效
            clip.start();
        } else {
            log.debug("重新播放");
            pausing = false;
            clip.setFramePosition(0);
            clip.start();//调用后,会减少[频繁开始播放]的丢失次数
            if (taskListener != null) taskListener.onStartPlay();
        }
    }

    @Override
    public void stop() {
        if (audioInfo == null) return;
        if (!pausing) {
            if (!clip.isRunning()) return;//没播放
            //播放中途,调用停止
            state.stopping = true;
            clip.stop();
        } else {
            //暂停中,调用停止
            afterStopped();
        }
    }

    private void afterStopped() {
        log.debug("播放-结束");
        state.reset();
        pausing = false;
        if (taskListener != null) taskListener.onStopped();
    }

    /**
     * @param isPause null-切换当前状态;true-暂停播放;false-恢复播放
     * @apiNote 对于很短的音效,在暂停后,调用本方法恢复播放时,高几率出现多次调用后才会恢复播放!Clip的bug
     */
    @Override
    public void pause(Boolean isPause) {
        if (audioInfo == null) return;

        /*
        ui一致的情况下,此按钮应该只在播放中途能触发
         */
        boolean applyPause = isPause == null ? !pausing : isPause;
        log.debug("applyPause={}", applyPause);
        if (applyPause) {
            if (!clip.isRunning()) {
                //要暂停,又没在播放/刚好播完了
                afterStopped();//最巧合时,会有两个onStopped事件,来自不同线程
                return;
            }
            state.pausing = true;
            clip.stop();
        } else {
            //要恢复播放
            if (clip.isRunning()) return;
            state.resuming = true;
            long framePosition = clip.getLongFramePosition();
            log.debug("resume, framePosition={}", framePosition);
            if (framePosition == clip.getFrameLength()) {
                clip.setFramePosition(0);
            }
            clip.start();
        }
    }

    @Override
    public void update(LineEvent event) {
        Type type = event.getType();
        log.debug("type={}", type);
        if (Type.START == type) {
            if (state.starting) {
                log.debug("开始播放");
                state.starting = false;
                if (taskListener != null) taskListener.onStartPlay();
            } else if (state.resuming) {
                log.debug("恢复播放");
                state.resuming = false;
                pausing = false;
                if (taskListener != null) taskListener.onPausedOrResume(false);
            }
        } else if (Type.STOP == type) {
            if (state.stopping) {
                afterStopped();
            } else if (state.pausing) {
                log.debug("暂停播放");
                state.pausing = false;
                pausing = true;
                if (taskListener != null) taskListener.onPausedOrResume(true);
            } else {
                if (!loop) {
                    //正常播放结束
                    log.debug("播放结束");
                    afterStopped();
                } else {
                    //循环播放时,会发送[结束-开始]的通知
                    afterStopped();
                    play();
                }
            }
        }
    }

    /**
     * @return 是否正在播放:false-已停止,暂停中;true-播放中
     */
    public boolean isPlaying() {
        return audioInfo != null && (clip.isRunning());
    }

    public void destroy() {
        clip.close();
    }

    /**
     * 操作(开始/停止/暂停/恢复等)提交中的状态,因为Clip有自己的异步线程,调用clip方法后会有延迟,
     * 所以需要此类
     */
    private static class ApplyingState {
        boolean starting;
        boolean stopping;

        boolean pausing;
        boolean resuming;

        public void reset() {
            starting = stopping = pausing = resuming = false;
        }
    }

}
